<?php
// +----------------------------------------------------------------------
// | 杭州十禾信息科技有限公司
// +----------------------------------------------------------------------
// | Copyright (c) 2013-2015 http://www.shelpe.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: LeePuman <ysyzlsm@163.com>
// +----------------------------------------------------------------------

Class SyncModel extends NewzanModel
{
	
	protected $tableName = 'sync_status';
	
	/**
	 * 同步用户的个人信息，当前余额等，同时会更新用户session('me')
	 * @param int $user_id , 用户淘宝id
	 * @param int $nick 用户淘宝昵称
	 * @param bool $force_sync , 是否强制同步，在conf文件中设置同步间隔，如果超过间隔就同步，如果为true时表示不管这个间隔强制同步，默认为false
	 * @return mixed, 同步成功时候返回同步的用户信息1d数组，示例如下，false表示同步失败
	 * 				array(
	 * 					'user_id' => 1231231,
	 * 					'nick' => 'taobaousernick',
	 * 					....
	 * 					'balance' => 12323
	 * 				)
	 */
	public function syncUser($user_id, $force_sync = false)
	{
		$data = array('init' => 1);
		$user = session('manager');
		
		if( !self::checkSyncStatus($user_id , 'user') && !$force_sync){
	        return 0;
	    }
		$userMdl = M('users');
		
		$resp = $this->_api->SimbaAccountBalanceGetRequest();
		$balance = $resp['balance'];
		$data['balance'] = $balance;
		
		$ret = $userMdl->where(array("user_id" => $user_id))->save( $data );
		//update user sesssion
		$me = session('me');
		$me = array_merge( $me , $data);
		session('me' , $me);
		//set user sync time
		$this->setLatestUpdated($user_id, 'user');
		 
		//update user 
		//return $data;
		return $me;
	}
	
	/**
	 * 同步用户所有的推广计划. 因为API是一次获取所有推广组, 因此暂不提供单个更新(TODO: 更新余额倒是可以).
	 * 
	 * @param int $user_id
	 * @param int $campaign_id  需要同步的campaign_id， 为0表示同步全部的推广计划. 目前只支持campaign_id=0.
	 * @param bool $force_sync  是否强制同步
	 * @return mixed|0  ,  返回一个更新完毕的campaign_id=>campaign数组. 如果没必要更新, 返回0.
	 */
	public function syncCampaign($user_id, $campaign_id = 0, $force_sync = false) //  , $campaign_id = 0 , $force_sync = false
	{ 
		if(empty($user_id)) 
			throw_exception('必须给定$user_id');
		if($campaign_id > 0) 
			throw_exception('目前只支持一次更新全部推广计划.');
		
		//$entity = 'campaign_' . $campaign_id;
		$entity = $campaign_id ? ('campaign:' . $campaign_id) : 'campaign';
		
	    // $updated = self::getLatestUpdated($user_id , $entity);
	    if( !self::checkSyncStatus($user_id , $entity) && !$force_sync){
	        return 0;
	    }
		$campaignMdl = new CampaignModel();
		$res = $this->_api->SimbaCampaignsGetRequest();
		$campaigns = (array) $res["campaigns"]->campaign;
		$sync_campaigns = array();
		foreach($campaigns as &$campaign){
			$campaign = (array) $campaign;
			$campaign['user_id'] = $user_id;
			$existentCampaign = $campaignMdl->getCampaignById($user_id, $campaign["campaign_id"], false);
			$existentCampaign = $existentCampaign ? $existentCampaign['campaign'] : null;
			if(!$existentCampaign) {
				$campaign["updated_time"] = date("Y-m-d H:i:s");
				$campaignMdl->add($campaign , array() , true);
				$campaignMdl->addLog($campaign["campaign_id"], CampaignLogActions::ADD,
						0, json_encode($campaign)); //先插入一条没有budget的记录
			}else{
			    //更新campaign，如果标题或者状态不一样的话
			   if( $existentCampaign['title'] != $campaign['title'] ){
			       $udpate_flag = true; 
				    $campaignMdl->addLog($campaign["campaign_id"], CampaignLogActions::UPDATE_TITLE,
						0, json_encode($campaign));
			   }
			   
			   if(  $existentCampaign['settle_status'] != $campaign['settle_status']  || 
			                   $existentCampaign['online_status'] != $campaign['online_status']){
			       $udpate_flag = true;
			       $campaignMdl->addLog($campaign["campaign_id"], CampaignLogActions::UPDATE_ONLINE_STATUS,
			               0, json_encode($campaign)); 
			   }
			   
			   if( $udpate_flag ){
			       $campaignMdl->where('campaign_id = ' . $campaign['campaign_id'] )->save( $campaign );
			   }
			   
			}
			// 利用getCampaignBudget()进行update操作. wired? 
			$campaign["budget"] = $campaignMdl->getCampaignBudget($user_id, $campaign["campaign_id"]);
			$sync_campaigns[ $campaign["campaign_id"] ] = $campaign;
		}
		
		//删除不存在的campaign
		$local_campaigns = M('campaigns')->where( 'user_id = ' . $user_id )->field('campaign_id')->select() ;
		foreach( $local_campaigns as $c){
		    //不存在就要删除
		    if( !isset($sync_campaigns[$c['campaign_id']]) ){
		        $ret = $campaignMdl->removeCampaign($user_id , $c['campaign_id']);
		        //同时删除相关同步数据
		        $whr = array(
					'user_id' => $user_id,
					'entity' => array('LIKE' , '%campaign%:'.$c['campaign_id']),
				);
		        M('sync_status')->where($whr)->delete();
		    }
		}
		
		//set sync time
		self::setLatestUpdated($user_id , $entity);		
		return $sync_campaigns;
	}
	
	
	/**
	 * 增量同步用户的adgroup，包括初始化adgroup，删除adgrup，更新adgroup的设置.
	 * 注意: 设置了更新时间间隔, 如果上次更新时间太短, 不会进行任何操作并返回0.
	 * 
	 * @param int $user_id ， 用户id
	 * @param int $campaign_id , adgroup的id，如果为0时表示同步该用户所有的adgroups
	 * @param string $force_sync ，是否强制同步,不管同步时间间隔,立即同步
	 * @return mixed, 返回0时表示无需同步(没有达到同步时间)
	 */
	public function syncAdgroup($user_id , $campaign_id = 0  , $force_sync = false)
	{
		$campaigns = array();
	    if(!$campaign_id){
	        $campaignMdl = new CampaignModel();
	        $campaigns = $campaignMdl->getCampaigns( $user_id , false );
	    }else{
	        $campaigns[] = array( 'campaign_id' => $campaign_id );
	    }

	    $adgroupMdl = new AdgroupModel();
	    $sync_adgroups = array();
	    $productMdl = new ProductModel();
	    $products = array();
	    
	    $camapagin_ids = array();
	    foreach($campaigns as $c){
	    	$camapagin_ids[] = $c['campaign_id'];
	    }
	    //获取全部本地的属于这个campaign的adgroups
	    $local_adgroups = array();
	    if(count($camapagin_ids)) {
		    $whr = array(
		    			'campaign_id' => array( 'IN' , $camapagin_ids)
		    		);
		    $local_adgroups = $adgroupMdl->where($whr)->select( array('assoc_key' => true) );
	    }
	    
	    $nothingUpdated = true;
	    $productMdl = new ProductModel();
	    foreach($camapagin_ids as $c_id){
	    	//获取adgroup同步时间,如果没有同步过,先开始初始化
	    	$entity = 'campaign-adgroup:' . $c_id;
	    	// 		$updated = self::getLatestUpdated($user_id , $entity);
	    	//设定更新阈值,防止多次反复更新
	    	if( !self::checkSyncStatus($user_id, $entity) && !$force_sync ){
	    		continue;
	    	} else {
	    		$nothingUpdated = false;
	    	}
	    	
	    	
			$params = array(
					'campaign_id' => $c_id,
					'page_size' => 200,
					'page_no' => 1
			        );
			$res = $this->_api->SimbaAdgroupsbycampaignidGetRequest($params);
			$tmp_adgroups = json_decode( json_encode( $res['adgroups']) , true);
			$online_adgrouops = isset($tmp_adgroups["adgroup_list"]["a_d_group"]) ? $tmp_adgroups["adgroup_list"]["a_d_group"] : array();
			$product_ids = array_column($online_adgrouops, 'num_iid');
			$me = session('me');
			$products = $productMdl->getProductByIds( $me['user_id'] , $product_ids , true);
			foreach($online_adgrouops as $ad){
			    /*
				$ad['user_id'] = $user_id;
				//检查是否存在推广组,存在就更新,不存在就添加
				$existentAdgroup = $adgroupMdl->getAdgroupByAdgroupId(
						$user_id, $ad['adgroup_id'], false);

				if($existentAdgroup) {
					$data = array_kv_diff($existentAdgroup, $ad);
					if($data) {
						$data["updated_time"] = date('Y-m-d H:i:s');
						$adgroupMdl->where(array('adgroup_id'=>$ad['adgroup_id']))->save($data);
						$adgroupMdl->addLog($ad['adgroup_id'], $ad['campaign_id'], AdgroupLogActions::MANUAL_UPDATE, 0, json_encode($data) );
					}
				} else {
					$ad["updated_time"] = date('Y-m-d H:i:s');
					$adgroupMdl->add($ad);
					$adgroupMdl->addLog($ad['adgroup_id'], $ad['campaign_id'], AdgroupLogActions::MANUAL_ADD,
							0, json_encode($ad) );
				}*/
			    
			    if( isset($local_adgroups[ $ad['adgroup_id'] ]) ){
			    	$data = array_kv_diff($local_adgroups[ $ad['adgroup_id'] ], $ad);
			    	if(count($data)>1){
			    		$adgroupMdl->addLog($ad['adgroup_id'], $ad['campaign_id'], AdgroupLogActions::MANUAL_UPDATE, 0, json_encode($data) );
			    	}
			    	unset( $local_adgroups[ $ad['adgroup_id'] ] );
			    }
			    //有时候会少个字段
			    if( !isset($ad['reason']) ) $ad['reason'] = '';
			    $ad['user_id'] = $user_id;
			    $ad["updated_time"] = date('Y-m-d H:i:s');
			  // dump( isset($products[ (string) $ad['num_iid'] ])  );
			    $pid = (string)  $ad['num_iid'] ;
			    if( isset($products[ $pid ])  ){
			        $ad['title'] = $products[ $pid ]['title'];
			        $ad['img_url'] = $products[ $pid ]['pic_url'];
			    }else{
			        $ad['title'] = $ad['title'];
			        $ad['img_url'] = $ad['img_url'];
			    }
			    ksort($ad); // 顺序要按照 key 值 ASCII 码升序排序
				$sync_adgroups[ $ad['adgroup_id'] ] = $ad;
			}
			//更新最后同步时间
			self::setLatestUpdated($user_id , $entity);
	    }
	    
	    //批量更新
	    $adgroupMdl->addAllOnDupUpdate(array_values($sync_adgroups));

	    if($nothingUpdated) {
	    	return 0;
	    }	    
	    
	    if(isset($local_adgroups)) {
		    foreach( $local_adgroups as $ad){
		        //如果不存在则需要删除并且写LOG
		        if( ! isset($sync_adgroups[$ad['adgroup_id']]) ){
		            $adgroupMdl->removeAdgroupFromDb($user_id, $ad['adgroup_id'] , $ad['campaign_id']);
		            
		        }
		    }
		}
	    return $sync_adgroups;	    
	}

	public function syncAdgroupPic($user_id)
	{
        $adgroupMdl = new AdgroupModel();
	    $productMdl = new ProductModel();

	    $local_adgroup_ids = $adgroupMdl
	    						->where("user_id = '".$user_id."'")
	    						->getField('adgroup_id,num_iid');
	    $local_prodcut_ids = $productMdl
	    						->where(array('user_id'=>$user_id))
	    						->getField('num_iid, pic_url');

		$updateDatas = array();
		foreach ($local_adgroup_ids as $adgroup_id => $num_id) {
			if(isset($local_prodcut_ids[$num_id])) {
				$updateDatas[] = array(
						'adgroup_id' => $adgroup_id,
						'img_url' => $local_prodcut_ids[$num_id]
					);
			}
		}

		$adgroupMdl->addAllOnDupUpdate($updateDatas, false, array('adgroup_id'));
	    return true;
	}

	/**
	 * 同步给定推广组的keyword. 如果上次更新时间很近, 并且未要求强制更新, 则不进行更新并返回0.
	 * @param int $user_id
	 * @param int $adgroup_id
	 * @param bool $force_sync
	 */
	public function syncKeywords($user_id , $adgroup_id , $force_sync = false)
	{
		if(empty($user_id) || $adgroup_id <= 0)
			throw_exception('参数错误: 必须全部给定且大于0.');
		 
		$entity = 'adgroup-keyword:' . $adgroup_id;
		$updated = self::getLatestUpdated($user_id , $entity);
		if( !self::checkSyncStatus($user_id , $entity) && !$force_sync){
			return 0;
		}
		
        $whr = array(
            'user_id' => $user_id,
            'adgroup_id' => $adgroup_id
        );
        
        $keywordMdl = new KeywordModel();
       	$local_keywords = $keywordMdl->getAdgroupKeywords($adgroup_id);
	    $resp = $this->_api->SimbaKeywordsByAdgroupIdGetRequest( array('adgroup_id' => $adgroup_id) );
        $keywords = isset($resp["keywords"]) && isset($resp["keywords"]->keyword) ? $resp["keywords"]->keyword : array();
        $updated_keywords = array();
        $added_keywords = array();
        $deleted_keywords = array();
        $api_keywords = array();
        foreach($keywords as $k){
        	$k = (array) $k;
        	//match_scope映射到matchscope， 原因是因为淘宝api返回来的是match_scope，我们的数据库字段是matchscope
        	//因为同时牵扯到其他地方插入matchscope到本地数据库的问题，暂时用此方法保证对其他代码影响最小 
        	$k['matchscope'] = $k['match_scope'];
        	//fix bug #0000674
        	$k['matchscope'] = empty($k['matchscope']) ? 4 : $k['matchscope']; 
        	$k['user_id'] = $user_id;
        	$str_keyword_id =  (string) $k['keyword_id'] ;
        	if( isset( $local_keywords[$str_keyword_id]) ){
        		$changed_data = array_kv_diff($local_keywords[$str_keyword_id]  , $k);
        		if(count($changed_data)){
        			$keywordMdl->where( 'keyword_id = ' . $k['keyword_id'])->save( $changed_data );

        			//增加改价LOG
        			if( isset($changed_data['max_price']) ){
        				$keywordMdl->addLog($k['keyword_id'], $k['word'], $adgroup_id,
        						KeywordLogActions::MANUAL_CHANGE_PRICE,
        						$changed_data['max_price']);
        			}

        			//增加修改MATCHSCOPE的LOG
        			if( isset($changed_data['matchscope']) ){
        				$keywordMdl->addLog($k['keyword_id'], $k['word'], $adgroup_id,
        						KeywordLogActions::MANUAL_CHANGE_MATCHSCOPE,
        						$changed_data['matchscope']);
        			}
        		}

        		// 剔除与线上一直的数据，剩下的表示表示已经被用户删除了
        		unset( $local_keywords[$str_keyword_id]  );
        		 
        	}else{
        		//增加的新词
        		$k['user_id'] = $user_id;
        		$keywordMdl->add($k);
        		//新增加词语LOG
        		$keywordMdl->addLog($k['keyword_id'], $k['word'], $adgroup_id ,
        				KeywordLogActions::MANUAL_NEW_KEYWORD,
        				$k['max_price'], json_encode($k)
        		);
        	}
        }
		//本地删除已经被用户删除的关键词
        if(count($local_keywords)>0){
            $del=array();
            foreach($local_keywords as $key=>$val){
                $del[]=$key;
                $keywordMdl->addLog($key, $val['word'], $adgroup_id, KeywordLogActions::MANUAL_DEL_KEYWORD);
            }
            $keywordMdl->deleteKeywordsFromDb($del);
        }    
		self::setLatestUpdated($user_id , $entity);
		return true;
	}

	public function syncKeywordsQscore($user_id, $adgroup_ids = array())
	{
		$keywordMdl = new KeywordModel();
		$adgroupMdl = new AdgroupModel();
		if(!$adgroup_ids) {
			$adgroup_ids = $adgroupMdl->where(array('user_id'=>$user_id))->getField('adgroup_id, num_iid');
			$adgroup_ids = array_keys($adgroup_ids);
		}
		$keywords =$keywordMdl->getField('keyword_id,adgroup_id');
		$init = array();
		foreach ($keywords as $keyword_id => $adgroup_id) {
			if(!isset($init[$adgroup_id])) {
				$init[$adgroup_id] = array();
			}
			if(in_array($adgroup_id, $adgroup_ids)) {
				$init[$adgroup_id][] = $keyword_id;
			}
		}

		foreach ($init as $adgroup_id => &$keywords) {
			$upData = array();
			$tmpArguments = array(
					'ad_group_id' => $adgroup_id
				);

			$keywords = array_chunk($keywords, 20);

			foreach ($keywords as &$keyword_ids) {
				$tmpArguments['bidword_ids'] = implode(',', $keyword_ids);
				$res = $this->_api->SimbaKeywordsQscoreSplitGetRequest($tmpArguments);
				$res = isset($res['result']->word_score_list->wordscorelist) ? $res['result']->word_score_list->wordscorelist : array();
				if($res) {
					foreach ($res as $key => $value) {
						$upData[] = array(
								'keyword_id' => $value->keyword_id,
								'wireless_qscore' => $value->wireless_qscore
							);
					}
				}
			}
			$keywordMdl->addAllOnDupUpdate($upData, array(), array('keyword_id'));
		}
		return true;
	}
	
	/**
	 * 辅助syncDeletedCreatives()和syncChangedCreatives, 在其之后使用.
	 * 检查adgroup是否有2个创意, 没有的话, 就同步其创意.
	 * 
	 * @param int $user_id
	 * @param string $last_updated 时间. 格式为YYYY-MM-DD HH:MM:SS
	 * @param CreativeModel $creative_mdl
	 */
	private function syncAddedCreatives($user_id, $creative_mdl)
	{
		$sql = 'SELECT adgroup_id, count(creative_id) as c_num FROM creatives where user_id=' . ((int) $user_id) 
			. ' group by adgroup_id having c_num < 4';
		$need_update_ads = $creative_mdl->query($sql);
		if(!$need_update_ads) 
			return 0;
		$affected = 0;
		foreach ($need_update_ads as $adgroup) {
			$adgroup_id = $adgroup['adgroup_id'];
			$affected += $creative_mdl->syncCreatives($user_id, $adgroup_id);
		}
		return $affected;
	}
	
	/**
	 * 同步给定用户给定时间之后删除掉的创意.
	 * 
	 * @param int $user_id
	 * @param string $last_updated 时间. 格式为YYYY-MM-DD HH:MM:SS
	 * @param CreativeModel $creative_mdl
	 * @return 删除掉的创意个数
	 */
	private function syncDeletedCreatives($user_id, $last_updated, $creative_mdl)
	{
		$page_no = 1;
		$page_size = 1000;
		$page_found = 1000;
		$total_item = 0;
		
		while($page_size == $page_found) {
			$resp = $this->_api->SimbaCreativeidsDeletedGetRequest( array( //SimbaCreativesChangedGetRequest
					'page_no' => $page_no,
					'page_size' => $page_size,
					'start_time' => $last_updated ));
			$resp = object_to_array($resp);
			$deleted_creatives = $resp['deleted_creative_ids']['number']; // IDs
			
			// ------- DEBUG----------
			// throw_exception('testing');
			
			$page_found = count($deleted_creatives);
			$total_item += $page_found;
				
			if(!$page_found)
				break;
			
			// 增加log
			foreach($deleted_creatives as $creative_id) {
				$adgroup_id = $creative_mdl->getAdgroupId($creative_id);
				if($adgroup_id == null) 
					$adgroup_id = 0;
				//如果$adgroup_id==0, 则表明该creative之前没有记录到数据库
				$creative_mdl->addLog($creative_id, $adgroup_id,
						CreativeLogActions::MANUAL_DEL, $creative_id);
			}
			
			// 保存删除的记录到删除表
			$str_creative_ids = join(',', $deleted_creatives);
			$sql = "INSERT INTO `creatives_deleted`
					(`creative_id`,`nick`,`campaign_id`,`adgroup_id`,`title`,`img_url`,`audit_status`,`audit_desc`,`create_time`,
					`modified_time`,`updated_time`,`user_id`)
					SELECT * FROM `creatives` WHERE user_id=$user_id AND creative_id IN ($str_creative_ids)";
			$creative_mdl->execute($sql);

			// 删除原记录
			$creative_mdl->where(array('user_id'=>$user_id, 'creative_id' => array('in', $deleted_creatives)))
					->delete();
			
			++$page_no;
		}
		
		return $total_item;
	}
	
	/**
	 * 同步给定用户给定时间之后修改过的创意
	 * 
	 * @param int $user_id
	 * @param string $last_updated 时间. 格式为YYYY-MM-DD HH:MM:SS
	 * @param CreativeModel $creative_mdl
	 * @return 修改过的创意个数
	 */
	private function syncChangedCreatives($user_id, $last_updated, $creative_mdl)
	{

		$page_no = 1;
		$page_size = 1000;
		$page_found = 1000;
		$total_item = 0;
		//$total_item = 1;
		//while(($page_no - 1) * $page_size < $total_item) {
		while($page_size == $page_found) {
			$resp = $this->_api->SimbaCreativeidsChangedGetRequest( array( //SimbaCreativesChangedGetRequest
					'page_no' => $page_no,
					'page_size' => $page_size,
					'start_time' => $last_updated ));
			$resp = object_to_array($resp);
		
			//$changed_creatives = $resp['creative_list']['a_d_group']; // array("creative_id"=>175148625,"modified_time"=>"2013-02-22 15:48:23", "nick"=>"妞妞猪猪516")
			$changed_creatives = $resp['changed_creative_ids']['number'];
			//$total_item = $resp['creatives']['total_item'];
			$page_found = count($changed_creatives);
			$total_item += $page_found;
			
			if(!$page_found)
				break;
			
			// 获取更新的creatvies详情, 保存到数据库
			$chunked_creative_ids = array_chunk($changed_creatives, 200); // TODO API是否能真正达到200条记录?
			foreach ($chunked_creative_ids as  $chunk) {
				$respCreatives = $this->_api->SimbaCreativesGetRequest(array(
						creative_ids => join(',', $chunk)
				));
				
				$creatives = $respCreatives['creatives']->creative;
				$creatives = object_to_array($creatives);
				foreach($creatives as &$c){
					$c['user_id'] = $user_id;

					$local_creative = $creative_mdl->getCreativeById($user_id, $c['creative_id']);
					$diff = array_kv_diff($local_creative, $c);
					if($diff) {
						$creative_mdl->addLog($c['creative_id'], $c['adgroup_id'], 
								CreativeLogActions::MANUAL_UPDATE, json_encode($diff));
					
						$addRes = $creative_mdl->add($c, array(), true);
						
						if($addRes === false)
							throw_exception('数据库错误: ' . $this->getDbError());
					}
					
					
				} //~foreach($creatives)
			}
			
			++$page_no;
			
		}
		
		return $total_item;
	}
	
	/**
	 * 同步给定用户的自上次同步以来所有的删除和修改过的推广组创意. 
	 * 主要用于脚本定时同步. 第一次初始化用户所有推广组创意需使用 SyncModel::syncUserCreatives().
	 * <strong>注意</strong>必须先同步了adgroups.
	 * 
	 * @see SyncModel::syncUserCreatives()
	 * @param int $user_id
	 * @return int
	 */
	public function syncCreatives($user_id)
	{
				
		if(!$user_id)
			throw_exception('参数$user_id错误.', RetStatus::PARAM_ERR);
		
		$entity = 'campaign-creative:%';
		$last_updated = self::getLatestUpdated($user_id, $entity);
		
		// 超过一个月的话, 不能再用增量更新. 必须全部重新获取.
		if($last_updated < date('Y-m-d H:i:s', time() - 29 * 86400)) {
			return $this->syncUserCreatives($user_id);
		}
		
		$creative_mdl = new CreativeModel();
		$creative_mdl->startTrans();
		$total_updated_item = 0;
		try {
			// ------- DEBUG----------
			$total_updated_item += $this->syncChangedCreatives($user_id, $last_updated, $creative_mdl);
			$total_updated_item += $this->syncDeletedCreatives($user_id, $last_updated, $creative_mdl);
			$total_updated_item += $this->syncAddedCreatives($user_id, $creative_mdl);

			$campaign_ids = D('Campaign')->where(array('user_id' => $user_id))->getField('campaign_id', true);
			foreach($campaign_ids as $campaign_id) {
				self::setLatestUpdated($user_id, "campaign-creative:$campaign_id");
			}
			
			$creative_mdl->commit();
		} catch (Exception $e) {
			$creative_mdl->rollback();
			throw $e;
		}

		return $total_updated_item;
	}
	
	/**
	 * 初始化用户的creatives. 2个参数的优先级从高到低分别为: $campaign_ids, $user_id.
	 * 该方法主要用户用户首次登陆初始化时使用. 后期同步调用SyncModel::syncCreatives(). 
	 * 注意: 需要先保证推广计划和推广组已经同步了.
	 * @see SyncModel::syncCreatives()
	 * @param int $user_id 用户ID
	 * @param array(int)|int $campaign_ids [可选] 如果给定, 则初始化给定的推广计划的创意.
	 * @return int 增加的creative记录条数.
	 * @throws ThinkException 数据库操作出错时抛出该错误.
	 */
	public function syncUserCreatives($user_id , $campaign_ids=0)
	{
		// public function initCreatives($user_id, $campaign_ids = 0) {
		if(!$user_id)
			throw_exception('参数$user_id错误.', RetStatus::PARAM_ERR);

		if($campaign_ids && !is_array($campaign_ids)) {
			$campaign_ids = array($campaign_ids);
		}
		if(!$campaign_ids) {
			$campaign_ids = D('Campaign')->where(array('user_id' => $user_id))->getField('campaign_id', true);
		}
		
		$added_num = 0;
		$adgroup_mdl = D('Adgroup');
		$creative_mdl = new CreativeModel();
		foreach($campaign_ids as $campaign_id) {
			
			$adgroup_ids = $adgroup_mdl
					->where(array('user_id' => $user_id, 'campaign_id' => $campaign_id))
					->getField('adgroup_id', true);
			
			if($adgroup_ids === false) {
				throw_exception('数据库错误: ' . $this->getDbError());
			}
			$sync_entity = "campaign-creative:$campaign_id";
			$added_num_in_a_campaign = 0;
			$creative_mdl->startTrans();
			try {
				foreach ($adgroup_ids as $adgroup_id) {
					$creative_mdl->syncCreatives($user_id, $adgroup_id);
				} //~foreach($adgroup_ids)
				self::setLatestUpdated($user_id, $sync_entity);
				$creative_mdl->commit();
				$added_num += $added_num_in_a_campaign;
			} catch (Exception $e) {
				$creative_mdl->rollback();
				throw $e;
			}
		} //~foreach($campaign_ids)
		
		return $added_num;
	}
	
	/**
	 * 同步给定用户的产品列表. 这个方法只能获取少量产品信息. 
	 * 详细的产品信息需要使用 SyncModel::syncProdcutDetails().
	 * 
	 * @see SyncModel::syncProdcutDetails()
	 * 
	 * @param int $user_id 用户ID, 当前session('me')必须是指向该用户.
	 * @param bool(default=true) $withDetail 是否同时更新产品详情
	 * @param bool(default=false) $force_sync 是否强制同步
	 * @param bool(default=true) $dynamic_update 是否动态更新<strong>产品详情</strong>. 注: 产品基本是肯定全部更新的.
	 */
	public function syncProducts($user_id, $withDetail = true, $force_sync = false, $dynamic_update = true)
	{
		$product_mdl = new ProductModel();
		
		$entity = 'product';

		if(!self::checkSyncStatus($user_id, $entity) && !$force_sync) // 1分钟
			return 0;
		
		// 同步基本信息
		$page = 1;
		$page_size = 200;
		$max_page= 50; // api limit
		$total_items = 0;
		
		$product_ids = array();
		
		do {
			$params = array(
					'page_size' => $page_size,
					'page_no' => $page,
			);
			$result = $this->_api->SimbaAdgroupOnlineitemsvonGetRequest($params);
			$result = object_to_array($result);
			$products = $result['page_item']['item_list']['subway_item'];
			if(isset($result["page_item"]['total_item']))
				$total_items = $result["page_item"]['total_item'];

			// dbg($products, '$products');
			// dbg($total_items, '$total_items');
			if($products) {
				// 将extra_attributes搬上一层
				foreach($products as &$product) {
					$product['num_iid'] = $product['num_id']; // SimbaAdgroupOnlineitemsvonGetRequest api返回的是num_id, 其他api返回的都是num_iid.
					if(strpos($product['img_url'], 'SS2_sum') !== false)
						$product['img_url'] = str_replace('.jpg', '.SS2', $product['img_url']);
					if(strpos($product['img_url'], 'gif_sum') !== false)
						$product['img_url'] = str_replace('.jpg', '.gif', $product['img_url']);
					$product['pic_url'] = str_replace(array('.jpg_sum', '.jpeg_sum', '.png_sum', '.SS2_sum','.gif_sum'), '', $product['img_url']); // $product['img_url'];
					$product['publish_time'] = $product['extra_attributes']['publish_time'];
					$product['quantity'] = isset($product['extra_attributes']['quantity']) ? $product['extra_attributes']['quantity'] : 0;
					$product['sales_count'] = isset($product['extra_attributes']['sales_count']) ? $product['extra_attributes']['sales_count'] : 0;
					unset($product['extra_attributes']);
					unset($product['num_id']);
					unset($product['img_url']);
					
					$product['user_id'] = $user_id;
					
					$product_ids[] = $product['num_iid'];
				}
				// 使用 insert on duplicat key update 代替
				$product_mdl->addAllOnDupUpdate($products, null, array('num_iid'));
			}
			
			++$page;
				
		} while($page_size * ($page - 1) <  $total_items && $page <= $max_page);
		
		// 删除不再存在的products
		$whr = array('user_id' => $user_id);
		if($product_ids) 
			$whr['num_iid'] = array('not in', $product_ids);
		$product_mdl->where($whr)->delete();
		
		// 同步详情
		if($withDetail) {
			$this->syncProdcutDetails($user_id, null, $dynamic_update);
		}
		
		self::setLatestUpdated($user_id, $entity);
		
		return $total_items;
	}
	
	/**
	 * 同步产品详情.
	 * 
	 * @param int $user_id
	 * @param bool $dynamic_update 是否动态更新, 如果true, 需要给定$prodcut_ids. 
	 * @param array(string) $prodcut_ids 全部新的产品ID. 
	 * 		当$dynamic_update为true时, 更新在$prodcut_ids且不在本地数据库中的产品;
	 * 		当$dynamic_update为false时, 如果给定$prodcut_ids, 更新全部在$prodcut_ids, 
	 * 		否则更新本地数据库中的产品.
	 * @return int 返回更新的产品数.
	 * 	
	 */
	public function syncProdcutDetails($user_id, $prodcut_ids = null, $dynamic_update = true)
	{
		$product_mdl = D('Product');
		// $local_prodcut_ids = $product_mdl->where(array('user_id'=>$user_id))->getField('num_iid', true);
		// sort($prodcut_ids);
		// sort($local_prodcut_ids);
		
		if(!$prodcut_ids) {
			$prodcut_ids = $dynamic_update ? 
					$product_mdl->where(array('user_id'=>$user_id, 'cid'=>0))->getField('num_iid', true) :
					$product_mdl->where(array('user_id'=>$user_id))->getField('num_iid', true);
		}
		
		Log::record(count($prodcut_ids) . ' products to update detail', Log::DEBUG);
		
		if(!$prodcut_ids)
			return 0;
		
		
		$page_size = 20; // api限制
		$page_count = ceil(count($prodcut_ids) / $page_size);
		$params = array('fields' => ProductModel::ITEM_GET_FIELDS);
		
		$updated_num = 0;
		$cates = array();
		$cate_mdl = M('tb_cates'); //new CategoryModel();
		for($page = 1; $page <= $page_count; ++$page) {
			$page_prodcut_ids = array_slice($prodcut_ids, ($page - 1) * $page_size, $page_size);
			Log::record('products: ' . implode(',' , $page_prodcut_ids), Log::DEBUG);
			$params['num_iids'] = implode(',' , $page_prodcut_ids);
			$resp = $this->_api->ItemsSellerListGetRequest($params);
			$resp = object_to_array($resp);
			$items = $resp['items']['item'];
			
			// dbg($items, '$items');
			// TODO 使用 insert on duplicat key update 代替
			foreach($items as &$item) {
				$cate_id = $item['cid'];
				if(!isset($cates[$cate_id])) {
					$cate_name = $cate_mdl->getFieldByCid($cate_id, 'name');
					if(!$cate_name) {
						Log::record("没有在categories表中找到id=$cate_id的记录", Log::WARN);
					}
					$cates[$cate_id] = $cate_name;
				}
				//$item['item_imgs'] = json_encode(array_values_recursive( $item['item_imgs'] ));
				$item['item_imgs'] = json_encode($item['item_imgs'] );
				$item['cat_name'] = $cates[$cate_id];
				$product_mdl->save($item);
			}
			
			// 因为api返回的结果有些字段为空, 需要处理后才能使用下面这个进行批量更新
			//$product_mdl->addAllOnDupUpdate($items, null, array('num_iid'));
			
			$updated_num += count($items);
		}
		
		return $updated_num;
	}
	
	public static function getLatestRun($user_id , $entity = null)
	{
		$model = M('sync_status');
		$whr = array('user_id' => $user_id);
		
		$whr['entity'] = is_array($entity) ? array('IN', $entity) : array('LIKE' , $entity);
		if($entity == 'rpt%')
			$whr['entity'] = array(
					array('LIKE' , $entity),
					array('NOTLIKE' , 'rpt-customer%')
				);
		$updated = $model->where($whr)->order('last_run_time ASC')->find();
		return $updated ? $updated['last_run_time'] : 0;
	}
	
	public static function getLatestUpdated($user_id , $entity = null)
	{
		$whr = array(
				'user_id' => $user_id,
				'entity' => array('LIKE' , $entity),
		);
		$updated = M('sync_status')->field('updated')->where($whr)->order('updated ASC')->find();
		return $updated ? $updated['updated'] : 0;
	}
	
	/** 设定用户某个实体最后更新时间
	 * @param int $user_id
	 * @param string $entity , 可以为
	 * 	  				'user' 用户信息更新（包括用户账户余额)
	 * 					'campaign' 推广计划的更新时间
	 * 					'adgroup' 推广组更新时间
	 * 					'catmatch' 类目出价更新时间
	 * 					'keyword' 关键词最后同步更新时间
	 * @param datetime $updated
	 * @return void
	 */
	public static function setLatestUpdated($user_id , $entity = 'user' , $updated = 0)
	{
        $data = array(
            'user_id' => $user_id,
            'entity' => $entity,
            'updated' => $updated ? $updated : date('Y-m-d H:i:s')
        );
        return M('sync_status')->add( $data , array(), true );
	}
	
	/**
	 * 检查给定项目是否有必要更新/同步.
	 * 
	 * @param int $user_id 用户ID
	 * @param string $entity 要检查的项目
	 * @param int $threshold 要检查的时间间隔(秒). 在这个时间范围内同步过的话, 就不再同步.
	 * @return bool true 表示可以更新, false表示没有超过阈值时间,不更新
	 */
	public static function checkSyncStatus($user_id, $entity, $threshold = 60)
	{
		$lastRunTime = M('sync_status')->where( array('user_id' => $user_id, 'entity' => $entity) )
				->getField('UNIX_TIMESTAMP(last_run_time)');
		$lastRunTime = $lastRunTime ? $lastRunTime : 0; // strtotime($lastRunTime)
	    return time() > $lastRunTime + $threshold;
	}
	
	/**
	 *  得到某元素或者报表需要更新的起止时间. 
	 *  
	 *  @param int $user_id
	 *  @param string $entity 格式如下: 
	 *  	单个: campaign-adgroup-creative:1933737 
	 *  	全部: campaign-adgroup-creative (不用复数)
	 * @return array|false 如果需要更新 , 返回数组, 包含两个元素: array('start'=>YYYY-mm-dd, 'end'=>YYYY-mm-dd)
	 * 		如果无需更新, 返回false.
	 */
	public static function getSyncTimeSlot($user_id, $entity)
	{
		$updated = self::getLatestUpdated($user_id , $entity);
		$startTime = $updated ? date("Y-m-d", strtotime($updated)) : date("Y-m-d", date2time(-90));
		// $endTime = date("Y-m-d" , strtotime('yesterday'));
		$res = array(
				'end' => date("Y-m-d" , strtotime('yesterday'))
			);
		if($startTime <= date("Y-m-d", date2time(-90))) { // <= 90天
			$res['start'] = date("Y-m-d", date2time(-90));
		} elseif ($startTime < date("Y-m-d", date2time(-30))) { // <= 30天
			$res['start'] = date("Y-m-d", date2time(-90));
		} elseif ($startTime < date("Y-m-d", date2time(-15))) { // <= 15天
			$res['start'] = date("Y-m-d", date2time(-30));
		} elseif ($startTime < date("Y-m-d", date2time(-7))) { // <= 7天
			$res['start'] = date("Y-m-d", date2time(-15));
		} elseif ($startTime < date("Y-m-d", date2time(-1))) { // < 1天
			$res['start'] = date("Y-m-d", date2time(-7));
		} else {
			$res['start'] = date("Y-m-d", date2time(-1));
		}
		return $res;
	}
}
